/*******************************************************************************************
 *	Copyright (c) 2016, zzg.zhou(11039850@qq.com)
 * 
 *  Monalisa is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU Lesser General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.

 *	This program is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU Lesser General Public License for more details.

 *	You should have received a copy of the GNU Lesser General Public License
 *	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *******************************************************************************************/
package com.tsc9526.monalisa.orm.datasource;

import com.tsc9526.monalisa.orm.Query;
import com.tsc9526.monalisa.orm.annotation.DB;
import com.tsc9526.monalisa.orm.dialect.Dialect;
import com.tsc9526.monalisa.orm.dialect.MysqlDialect;
import com.tsc9526.monalisa.orm.dialect.OracleDialect;
import com.tsc9526.monalisa.orm.dialect.PostgresDialect;
import com.tsc9526.monalisa.orm.dialect.SQLServerDialect;
import com.tsc9526.monalisa.orm.model.Model;
import com.tsc9526.monalisa.orm.model.ModelEvent;
import com.tsc9526.monalisa.orm.model.ModelMeta;
import com.tsc9526.monalisa.orm.model.Record;
import com.tsc9526.monalisa.tools.cache.Cache;
import com.tsc9526.monalisa.tools.cache.CacheManager;
import com.tsc9526.monalisa.tools.datatable.DataMap;
import com.tsc9526.monalisa.tools.datatable.DataTable;
import com.tsc9526.monalisa.tools.datatable.Page;
import com.tsc9526.monalisa.tools.io.MelpClose;
import com.tsc9526.monalisa.tools.logger.Logger;
import com.tsc9526.monalisa.tools.string.MelpString;

import javax.sql.DataSource;
import java.io.Closeable;
import java.lang.annotation.Annotation;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/** 
 * 
 * @author zzg.zhou(11039850@qq.com)
 */
public class DBConfig implements Closeable{ 	
	static Logger logger=Logger.getLogger(DBConfig.class);
	 
	public final static String PREFIX_DB       ="DB";
	public final static String CFG_DEFAULT_NAME="cfg";
	
	private String[] prefixs=new String[]{PREFIX_DB+"."+CFG_DEFAULT_NAME};
	
	protected CFG _cfg=new CFG();
	 
	private final DataSource ds;

	/**
	 * DB Name
	 */
	private String schema;

	/**
	 * 数据库方言
	 */
	private Dialect dialect;

	private boolean initialized=false;


	public DBConfig(DataSource ds) {
		this("default", ds);
	}

	public DBConfig(String key, DataSource ds) {
		this._cfg.key=key;
		this.ds = ds;
	}

	public DBConfig(String key, DataSource ds, DB db){
		this.ds = ds;
		this._cfg.key=key;
		this._cfg.db=db;
	}
	
	public CFG getCfg(){
		if(!initialized){
			init();			 
		}
		return _cfg;
	}

	 
	synchronized void init(){
		Connection connection = null;
		try {
			connection = getDataSource().getConnection();
			schema = connection.getCatalog();

			DatabaseMetaData metaData = connection.getMetaData();
			String databaseProductName = metaData.getDatabaseProductName().toUpperCase();
			if(databaseProductName.contains("MYSQL")) {
				dialect = new MysqlDialect();
			} else if(databaseProductName.contains("ORACLE")) {
				dialect = new OracleDialect();
			} else if(databaseProductName.contains("POSTGRES")) {
				dialect = new PostgresDialect();
			} else if(databaseProductName.contains("SQLSERVER")) {
				dialect = new SQLServerDialect();
			}
		} catch (SQLException e) {
			logger.info("can't not find schema. cause: " + e.getMessage());
		} finally {
			if(connection != null) {
				try {
					connection.close();
				} catch (Exception e) {}
			}
		}
		initialized=true;
	}
	
	synchronized void init(DB db){
		initialized=true;
	}
	
	public Dialect getDialect(){
		return dialect;
	}
 	

	public String getSchema(){
		return schema;
	}
	
	public String getKey(){
		return getCfg().getKey();
	}
	
	public DB getDb(){
		return getCfg().getDb();
	}
	
	protected synchronized void delayClose(final DataSource ds,int delay){
		ModelMeta.clearReloadModelMetas(getKey());
		
		MelpClose.delayClose(ds,delay);
	}
	 		
	public DataSource getDataSource(){
		return ds;
	}

	protected void finalize()throws Throwable{
		close();
		super.finalize();
	}


	@Override
	public synchronized void close(){
		if(ds!=null){
			MelpClose.close(ds);
		}
	}	 

	public Record createRecord(String tableName,String ... primaryKeys){		
		Record m=new Record(tableName,primaryKeys);
		m.use(this);
		return m;
	}
	
	public Query createQuery(){		
		return new Query(this);
	}
	 
	public DataTable<DataMap> select(String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getList();
	}
	 
	public DataTable<DataMap> select(int limit, int offset,String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getList(limit, offset);
	}
	
	public <T> DataTable<T> select(Class<T> resultClass,String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getList(resultClass);
	}
	
	public <T> Page<T> select(Class<T> resultClass,int limit, int offset,String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getPage(resultClass,limit, offset);
	}
	
	
	public DataMap selectOne(String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getResult();
	}
	 
	public <T> T selectOne(Class<T> resultClass,String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getResult(resultClass);
	}
	
	public Page<DataMap> selectPage(int limit, int offset,String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getPage(limit, offset);
	}
	 
	public <T> Page<T> selectPage(Class<T> resultClass,int limit, int offset,String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.getPage(resultClass,limit, offset);
	}
	
	public int execute(String sql,Object... args){
		Query query=createQuery().add(sql,args);
		return query.execute();
	}
		
	public int[] executeBatch(String[] sqls){
		Query query=createQuery();
		return query.executeBatch(sqls);
	}
	
	public int[] executeBatch(String sql,List<Object[]> args){
		Query query=createQuery();
		query.add(sql);
		 
		return query.executeBatch(args);
	}
	
	
	public int[] batchInsert(List<? extends Model<?>> models){
		return batchOpModels(models, ModelEvent.INSERT); 
	}
	
	public int[] batchReplace(List<? extends Model<?>> models){
		return batchOpModels(models, ModelEvent.REPLACE); 
	}
	
	public int[] batchUpdate(List<? extends Model<?>> models){
		return batchOpModels(models, ModelEvent.UPDATE); 
	}
	 
	public int[] batchDelete(List<? extends Model<?>> models){
		return batchOpModels(models, ModelEvent.DELETE); 
	}
	
	protected int[] batchOpModels(final List<? extends Model<?>> models,final ModelEvent op){
		return com.tsc9526.monalisa.orm.Tx.execute(new com.tsc9526.monalisa.orm.Tx.Atom<int[]>(){
			public int[] execute() throws Throwable {
				int[] rs=new int[models.size()];
				int i=0;
				for(Model<?> m:models){
					int r=0;
					
					if(op==ModelEvent.INSERT){
						r=m.save();
					}else if(op==ModelEvent.REPLACE){
						r=m.saveOrUpdate();
					}else if(op==ModelEvent.UPDATE){
						r=m.update();
					}else if(op==ModelEvent.DELETE){
						r=m.delete();
					}
					
					rs[i++]=r;
				}
				return rs;
			}
		});
	}
	
	public boolean tableExist(String name){
		return tableExist(name, false);
	}
	
	public boolean tableExist(String name,boolean includeView){
		return getDialect().tableExist(this, name, includeView);
	}
	
	public Set<String> getTables(){
		return getTables(false);
	}
	
	public Set<String> getTables(boolean includeView){
		String[] types=includeView? new String[]{"TABLE","VIEW"}  : new String[]{"TABLE"};
		
		Connection conn=null;
		ResultSet rs=null;
		try{
			conn=getDataSource().getConnection();
			 
			Set<String> tables=new LinkedHashSet<String>();
			 
			DatabaseMetaData dbm=conn.getMetaData();
			rs=dbm.getTables(null, null,"%",types );
			while(rs.next()){
				String table=rs.getString("TABLE_NAME");
				table=Dialect.getRealname(table);
				
				tables.add(table.toUpperCase());
			}
			return tables;
		}catch(SQLException e){
			throw new RuntimeException(e);
		}finally{
			MelpClose.close(rs,conn);
		}
	}


	/**
	 * 通过 DB Class 创建 DBConfig
	 * @param clazzWithDBAnnotation
	 * @return
	 */
	public static DBConfig fromClass(Class<?> clazzWithDBAnnotation){
		return DataSourceManager.getInstance().getDBConfig(clazzWithDBAnnotation);
	}


	/**
	 * 通过 DATASource 创建 DBConfig
	 * @param ds
	 * @return
	 */
	public static DBConfig fromDataSource(DataSource ds){
		return fromDataSource("default", ds);
	}



	/**
	 * 通过 DATASource 创建 DBConfig
	 * @param ds
	 * @return
	 */
	public static DBConfig fromDataSource(String dbKey, DataSource ds){
		return DataSourceManager.getInstance().getDBConfig(dbKey, ds);
	}

	/**
	 * 通过 JDBC prop 创建
	 * @param driverClass
	 * @param jdbcUrl
	 * @param username
	 * @param password
	 * @return
	 */
	public static DBConfig fromJdbcUrl(String driverClass, String jdbcUrl,String username,String password){
		final String dbKey=jdbcUrl+"&username="+username+"&password="+password;
		DB db=createDB(driverClass, jdbcUrl,username,password);
		return DataSourceManager.getInstance().getDBConfig(dbKey, db);
	}
	
	public static DB createDB(final String driverClass, final String jdbcUrl, final String username,final String password){
		final String dbKey=jdbcUrl+"&username="+username+"&password="+password;
		return new DB(){
			public Class<? extends Annotation> annotationType() {
				return DB.class;
			}
			 
			public String url() {
				return jdbcUrl;
			}
	 	 
			public String driver() {
				return driverClass;
			}
	
			public String username() {
				return username;
			}
	
			public String password() {
				return password;
			}

	
			public String key() {
				return dbKey;
			}
	 	 
			public Class<? extends ConfigClass> configClass() {
				return null;
			}
			
			public String[] properties(){
				return new String[]{};
			}
		};
	}
	
	
	public class CFG{
		protected String cfgBasePath =null;

		private DB     db;
		private String key;

		private Properties p=new Properties();
		 
		private Map<String,String> cacheModels=new ConcurrentHashMap<String,String>();
		private String cacheTables;

		private long lastestLoadCfgTime = 0L;
		
		synchronized void init(){
			loadProperties();
			this.cacheTables	 = getValue(p,DbProp.PROP_DB_CACHE_TABLES.getKey(), "",  prefixs);
		}

		
		private void loadProperties(){
			lastestLoadCfgTime = System.currentTimeMillis();
			
			String[] props=db.properties();
			if(props!=null){
				for(String pv:props){
					if(pv!=null){
						int x=pv.indexOf("=");
						if(x>0){
							String name=PREFIX_DB+"."+CFG_DEFAULT_NAME+"."+pv.substring(0,x).trim();
							String value=pv.substring(x+1).trim();
							if(!p.containsKey(name)){
								p.put(name, value);
							}
						}
					}
				}
			}
		}
		


		protected String getValue(Properties p,String key,String defaultValue,String[] prefixs){
			String r=null;
			if(prefixs.length>0){
				for(String prefix:prefixs){
					String pk = prefix+"."+key;
					if(!p.containsKey(pk)) {
						pk = prefix.toLowerCase()+"."+key;
					}
					
					String v=p.getProperty(pk);
					if(v!=null){
						r=v;
						break;
					}
				}
			}
			
			if(r==null){
				r= p.getProperty(key, defaultValue);
			}
			
			if(r!=null){
				r=r.trim();
			}
			return r;
			
		}
		
		public String getProperty(String key){				
			return this.getValue(p, key, null, prefixs);		
		}

		
		public String getProperty(String key,String defaultValue){		 
			return this.getValue(p, key, defaultValue, prefixs);
		}
		
		public int getProperty(String key,int defaultValue){		 		
			String v=getProperty(key);
			if(v==null || v.trim().length()==0){
				return defaultValue;
			}else{
				return Integer.parseInt(v.trim());
			}
		}
		
		public Cache getCache(Model<?> m){
			if(cacheTables!=null && cacheTables.trim().length()>0 && cacheModels.size()==0){
				for(String name:MelpString.splits(cacheTables)){
					String ckey=name.trim();
					String cname=DbProp.PROP_TABLE_CACHE_NAME.getValue(DBConfig.this, ckey);
					 
					ckey=getDialect().getTableName(ckey).toLowerCase();
					 
					cacheModels.put(ckey, cname);
				}
			}

			String tableName=m.table().name();
			
			String cacheName=getDialect().getTableName(tableName).toLowerCase();
			if(cacheModels.containsKey(cacheName)){
				String name=cacheModels.get(cacheName);
				
				String cacheClass=DbProp.PROP_TABLE_CACHE_CLASS.getValue(DBConfig.this, tableName);
				String eviction  =DbProp.PROP_TABLE_CACHE_EVICTION.getValue(DBConfig.this, tableName);
				
				return CacheManager.getInstance().getCache(cacheClass,eviction,name);
			}else{
				return null;
			}
		}
	 	
		public Cache getCache(){
			String cacheClass=DbProp.PROP_TABLE_CACHE_CLASS.getValue(DBConfig.this);
			String eviction  =DbProp.PROP_TABLE_CACHE_EVICTION.getValue(DBConfig.this);
			String name      =DbProp.PROP_TABLE_CACHE_NAME.getValue(DBConfig.this);
			
			return CacheManager.getInstance().getCache(cacheClass,eviction,name);
		}
		
		public Cache getCache(String name){
			String cacheClass=DbProp.PROP_TABLE_CACHE_CLASS.getValue(DBConfig.this);
			String eviction  =DbProp.PROP_TABLE_CACHE_EVICTION.getValue(DBConfig.this);
			 
			return CacheManager.getInstance().getCache(cacheClass,eviction,name);
		}
		

		public DB getDb() {
			return db;
		}
		
		public String getKey() {
			return key;
		}

		
		public Properties getProperties() {
			return p;
		}
		
	}
}
